package erfgame.core.world.generator;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


import erfgame.core.BaseCompositeOperation;
import erfgame.core.Operation;
import erfgame.core.Point3D;
import erfgame.core.Point3DAndRotation;
import erfgame.core.Rectangle3D;
import erfgame.core.Rotation;
import erfgame.core.SimpleAbstractOperation;
import erfgame.core.dao.ObjectDAO;
import erfgame.core.event.queue.memory.InMemoryEventQueue;
import erfgame.core.memory.MemoryPool;
import erfgame.core.memory.Pointer;
import erfgame.core.world.World;
import erfgame.core.world.WorldDAO;
import erfgame.core.world.WorldDAOManager;
import erfgame.core.world.WorldGenerationParameters;
import erfgame.core.world.WorldGenerator;
import erfgame.core.world.WorldUpdateDescription;
import erfgame.core.world.generator.terrain.CompositeWorldGenerator;
import erfgame.core.world.generator.terrain.LayeringTerrainBlockGeneratorProxy;
import erfgame.core.world.generator.terrain.SinusoidalTerrainBlockGenerator;
import erfgame.core.world.generator.terrain.SlopedTerrainBlockGenerator;
import erfgame.core.world.generator.terrain.TerrainBlockGenerator;
import erfgame.core.world.generator.terrain.TerrainBlockWorldGenerator;
import erfgame.core.world.terrain.BlockGridTiles;
import erfgame.core.world.terrain.BlockSurfaceImageFactory;
import erfgame.core.world.terrain.BlockSurfaceImageFactoryManager;
import erfgame.core.world.terrain.BlockSurfaceImages;
import erfgame.core.world.terrain.TerrainConstants;
import erfgame.core.world.terrain.TerrainUtils;
import erfgame.core.world.terrain.rotated.RotatedWorld;
import erfgame.core.world.terrain.rotated.TransformedBlock;

public class WorldGeneratorUtils {
	
	@SuppressWarnings(value="unchecked")
	public static final Operation<Void, RuntimeException> generate( 
			final WorldDAOManager worldDAOManager, 
			BlockSurfaceImageFactoryManager imageFactoryManager, 
			WorldGenerationParameters parameters, 
			MemoryPool<byte[][][]> terrainMemoryPool
	) {
		final World world = WorldGeneratorUtils.createWorld( worldDAOManager, parameters );
		
		final WorldDAO worldDAO = worldDAOManager.getWorldDAO();
		
		TerrainBlockGenerator terrainBlockGenerator;
		terrainBlockGenerator = new SinusoidalTerrainBlockGenerator( 
				400, 
				25, 
				(parameters.getBlockDimensions().depth * parameters.getWorldBlocks().depth) / 2, 
				TerrainConstants.TERRAIN_TYPE_AIR, 
				TerrainConstants.TERRAIN_TYPE_SOIL
//				TerrainConstants.TERRAIN_TYPE_SNOW 
		);
		
		int ww = parameters.getWorldBlocks().width;
		int wh = parameters.getWorldBlocks().height;
		int wd = parameters.getWorldBlocks().depth;
		
		int bw = parameters.getBlockDimensions().width;
		int bh = parameters.getBlockDimensions().height;
		int bd = parameters.getBlockDimensions().depth;
		
		int minDimension = Math.min( bw*ww, Math.min(bh*wh, bd*wd));
		
		final BlockSurfaceImageFactory blockSurfaceImageFactory;
		blockSurfaceImageFactory = imageFactoryManager.getFactory(world); 
		
		ObjectDAO<Point3D, Pointer<byte[][][]>> blockTerrainDAO;
		blockTerrainDAO = worldDAOManager.getBlockTerrainDAO(world);
		WorldGenerator terrainGenerator = new TerrainBlockWorldGenerator(
				worldDAOManager, 
//				new LayeringTerrainBlockGeneratorProxy(
//						terrainBlockGenerator,
//						blockTerrainDAO,
//						5, 
//						TerrainConstants.TERRAIN_TYPE_SAND,
//						world.getBlocksZ()
//				),
//				new SphereTerrainBlockGenerator(
//						TerrainConstants.TERRAIN_TYPE_SNOW, 
//						(ww*bw)/2, 
//						(wh*bh)/2, 
//						(wd*bd)/2, 
//						minDimension/2
//				),
//				new CylinderTerrainBlockGenerator(
//						(ww*bw)/2, 
//						(wh*bh)/2, 
//						Math.min((ww-2)*bw, (wh-2)*bh)/2,
//						parameters.getBlockDimensions().depth, 
//						(parameters.getWorldBlocks().depth-2)*parameters.getBlockDimensions().depth,
//						TerrainConstants.TERRAIN_TYPE_SNOW
//				),
//				new FlatTerrainBlockGenerator(TerrainConstants.TERRAIN_TYPE_SNOW),
//				new SlopedTerrainBlockGenerator(
//						Math.PI/2 + Math.PI/100, 
//						-Math.PI/2 + Math.PI/100, 
//						(parameters.getBlockDimensions().width * parameters.getWorldBlocks().width)/2,
//						0,//(parameters.getBlockDimensions().depth * parameters.getWorldBlocks().depth) / 2, 
//						TerrainConstants.TERRAIN_TYPE_SNOW),
				new SlopedTerrainBlockGenerator(
						0, 
						0, 
						0,
						(parameters.getBlockDimensions().depth * (parameters.getWorldBlocks().depth-2)), 
						TerrainConstants.TERRAIN_TYPE_SAND),

//				new HorizontalCylinderTerrainBlockGenerator(
//						(wh*bh)/2, 
//						(wd*bd)/2, 
//						Math.min(wh*bh, wd*bd)/2-1, 
//						parameters.getBlockDimensions().width, 
//						(parameters.getWorldBlocks().width-2)*parameters.getBlockDimensions().width, 
//						TerrainConstants.TERRAIN_TYPE_SNOW
//				),
//				new BrickTerrainBlockGenerator(
//						TerrainConstants.TERRAIN_TYPE_SNOW, 
//						parameters.getBlockDimensions().width, 
//						parameters.getBlockDimensions().height, 
//						parameters.getBlockDimensions().depth, 
//						(parameters.getWorldBlocks().width-2)*parameters.getBlockDimensions().width, 
//						(parameters.getWorldBlocks().height-2)*parameters.getBlockDimensions().height, 
//						(parameters.getWorldBlocks().depth-2)*parameters.getBlockDimensions().depth
//				),
				terrainMemoryPool
		);
		
		CompositeWorldGenerator worldGenerator = new CompositeWorldGenerator();
		worldGenerator.addChild( terrainGenerator );


		Operation<Void, RuntimeException> preliminaryOperation = new SimpleAbstractOperation<Void, RuntimeException>("initialising") {

			@Override
			protected Void doPerform() throws RuntimeException {
				worldDAO.storeWorld( world );
				return null;
			}
		};
		Operation<Void, RuntimeException> operation = worldGenerator.generate(world, parameters);
		Operation<Void, RuntimeException> cleanUpOperation = new SimpleAbstractOperation<Void, RuntimeException>("saving") {

			@Override
			protected Void doPerform() throws RuntimeException {
		
				// write everything back
				WorldUpdateDescription update;
				update = new WorldUpdateDescription(
						new Rectangle3D( 
								0, 0, 0,
								world.getBlocksX()-1,
								world.getBlocksY()-1,
								world.getBlocksZ()-1
						),
						WorldUpdateDescription.FLAG_ENTITY_ADDED | 
						WorldUpdateDescription.FLAG_TERRAIN_APPEARANCE_CHANGE |
						WorldUpdateDescription.FLAG_TERRAIN_SHAPE_CHANGE
				);
				WorldGeneratorUtils.update(world, update, blockSurfaceImageFactory, worldDAOManager);
				return null;
			}
		};
		BaseCompositeOperation<Void, Void, RuntimeException> result = new BaseCompositeOperation<Void, Void, RuntimeException>(
				Arrays.asList(preliminaryOperation, operation, cleanUpOperation)
		);
		return result;
	}
	
	public static final int getNextWorldId(WorldDAOManager worldDAOManager ) {
		List<World> worlds = worldDAOManager.getWorldDAO().getWorlds();
		int maxWorldId = 0;
		for( int i = worlds.size(); i>0; ) {
			i--;
			World world = worlds.get(i);
			int id = world.getWorldId();
			if( id > maxWorldId ) {
				maxWorldId = id;
			}
		}
		return maxWorldId+1;
	}
	
	public static final World createWorld( WorldDAOManager worldDAOManager, WorldGenerationParameters parameters ) {
		int worldId = getNextWorldId(worldDAOManager);
		
		World world = new World();
		world.setWorldId( worldId );
		world.setWorldName( parameters.getWorldName() );
		world.setMaxShadowDepth(80);
		
		world.setBlockWidth( parameters.getBlockDimensions().width );
		world.setBlockHeight( parameters.getBlockDimensions().height );
		world.setBlockDepth( parameters.getBlockDimensions().depth );
		
		world.setBlocksX( parameters.getWorldBlocks().width );
		world.setBlocksY( parameters.getWorldBlocks().height );
		world.setBlocksZ( parameters.getWorldBlocks().depth );
		
		world.setGridDiagonal( parameters.getGridDiagonal() );
		
		world.setEventQueue( new InMemoryEventQueue() );
		
		world.setWorldTime( 0 );
		
		return world;
	}
	
//	public static void updateSurfaces( World world, int bx, int by, int bw, int bh  ) {
//		byte[][][] terrain = null;
//		ObjectDAO<Point3D, byte[][][]> terrainDAO;
//		terrainDAO = this.worldDAOManager.getBlockTerrainDAO(world);
//		TerrainUtils.regenerateSurfaces(
//				bx, by, bw, bh, 
//				this.worldDAOManager.getSurfacesDAO(world), 
//				terrainDAO, 
// 				terrain, 
//				world
//		);		
//	}
	
	@SuppressWarnings("unchecked")
	public static void updateSurfaceImages( 
			World world, 
			ObjectDAO<Point3D, Pointer<byte[][][]>> blockTerrainDAO, 
			ObjectDAO<Point3DAndRotation, BlockSurfaceImages> surfaceImagesDAO, 
			ObjectDAO<Point, int[][][]> surfacesDAO,
			BlockSurfaceImageFactory surfaceImageFactory, 
			int bx, int by, int bz, 
			int bw, int bh, int bd 
	) {
//		ObjectDAO<Point3D, byte[][][]> blockTerrainDAO = this.worldDAOManager.getBlockTerrainDAO( world );
//		ObjectDAO<Point3D, BlockSurfaceImages> surfaceImagesDAO = this.worldDAOManager.getBlockSurfaceImagesDAO( world );
		
		int blocksX = world.getBlocksX();
		int blocksY = world.getBlocksY();
		int blocksZ = world.getBlocksZ();
		
		byte[][][][][][] rawTerrain = new byte[3][3][3][][][];
		int[][][][][] surfaces = new int[3][3][][][];

		Point midPoint = new Point(2, 2);
		TransformedBlock<byte[]>[][][] terrain = new TransformedBlock[3][3][3];
		TransformedBlock<TransformedBlock<byte[]>[]> rotatedTerrain = new TransformedBlock<TransformedBlock<byte[]>[]>(
			terrain, 
			3, 3
		);
		RotatedWorld rotatedWorld = new RotatedWorld(world, Rotation.degrees0);
		// for each block generate the world 
		ArrayList<Pointer<byte[][][]>> pointers = new ArrayList<Pointer<byte[][][]>>(9);
		for( int x=bx+bw; x>bx; ) {
			x--;
			for( int y=by+bh; y>by; ) {
				y--;
				for( int z=bz+bd; z>bz; ) {
					z--;
					for( int dx=3; dx>0; ) {
						dx--;
						int tx = x + dx - 1;
						for( int dy=3; dy>0; ) {
							dy--;
							int ty = y + dy - 1;
							if( tx >= 0 && tx < blocksX && 
								ty >= 0 && ty < blocksY ) 
							{
								for( int dz=3; dz>0; ) {
									dz--;
									int tz = z + dz - 1;
									TransformedBlock<byte[]> block;
									byte[][][] blockTerrain;
									if( tz >= 0 && tz < blocksZ ) {
										Pointer<byte[][][]> pointer = blockTerrainDAO.retrieveObject( new Point3D(tx, ty, tz) );
										pointers.add( pointer );
										blockTerrain = pointer.getValue();
										block = new TransformedBlock<byte[]>(
												blockTerrain, 
												world.getBlockWidth(), 
												world.getBlockHeight()
										);
									} else {
										block = null;
										blockTerrain = null;
									}
									rawTerrain[dx][dy][dz] = blockTerrain;
									terrain[dx][dy][dz] = block;
								}
								Point surfaceKey = new Point(tx, ty);
								surfaces[dx][dy] = surfacesDAO.retrieveObject(surfaceKey);
							}
						}
					}
					// render the terrain
					for( Rotation rotation : Rotation.values() ) {
						rotatedWorld.setRotation( rotation );
						// set all the blocks to being rotated this way
						rotatedTerrain.setRotation(
								rotation, 
								rotation.getRotateInBoxTransformation(midPoint.x, midPoint.y)
						);
						for( int tx=terrain.length; tx>0; ) {
							tx--;
							TransformedBlock<byte[]>[][] terrainX = terrain[tx];
							for( int ty=terrainX.length; ty>0; ) {
								ty--;
								TransformedBlock<byte[]>[] terrainXY = terrainX[ty];
								for( int tz=terrainXY.length; tz>0; ) {
									tz--;
									TransformedBlock<byte[]> terrainXYZ = terrainXY[tz];
									if( terrainXYZ != null ) {
										terrainXYZ.setRotation(
												rotation, 
												rotation.getRotateInBoxTransformation(world.getBlockWidth()-1, world.getBlockHeight()-1)
										);
									}
								}
							}
						}
						try {
							Point3DAndRotation key = new Point3DAndRotation(x, y, z, rotation);
							
							BlockSurfaceImages surfaceImages = surfaceImageFactory.render( 
									1, 
									1, 
									1,
									rotatedTerrain,
									rawTerrain,
									surfaces,
									key, 
									rotatedWorld, 
									z == 0 
							);
							surfaceImagesDAO.storeObject( key, surfaceImages );
						} catch( Exception ex ) {
							throw new RuntimeException( "unable to create block "+bx+","+by+","+bz+" "+rotation.getDegrees(), ex );
						}
					}
					for( int i=pointers.size(); i>0; ) {
						i--;
						pointers.get( i ).free();
					}
					pointers.clear();
				}
			}
		}
	}
	
	public static void update( 
			World world, 
			WorldUpdateDescription update,
			BlockSurfaceImageFactory bsiFactory,
			WorldDAOManager worldDAOManager
	) {
		ObjectDAO<Point, int[][][]> surfacesDAO = worldDAOManager.getSurfacesDAO(world);
		ObjectDAO<Point, BlockGridTiles> gridTilesDAO = worldDAOManager.getGridTilesDAO(world);
		ObjectDAO<Point3D, Pointer<byte[][][]>> terrainDAO = worldDAOManager.getBlockTerrainDAO(world);
		ObjectDAO<Point3DAndRotation, BlockSurfaceImages> bsiDAO = worldDAOManager.getBlockSurfaceImagesDAO(world);
		ObjectDAO<Point, int[][][]> surfaceDAO = worldDAOManager.getSurfacesDAO(world);

		Rectangle3D area = update.getArea();
		int flags = update.getFlags();
		if( (flags & WorldUpdateDescription.FLAG_TERRAIN_SHAPE_CHANGE) != 0 ) {
			TerrainUtils.regenerateSurfaces(
					area.minX, 
					area.minY, 
					area.getWidth(), 
					area.getHeight(), 
					surfacesDAO, 
					terrainDAO, 
	 				null, 
					world
			);		
			TerrainUtils.regenerateGridTiles(
					area.minX, 
					area.minY, 
					area.getWidth(), 
					area.getHeight(), 
					surfacesDAO, 
					gridTilesDAO, 
					world
			);
		}
		if( (flags & (WorldUpdateDescription.FLAG_TERRAIN_APPEARANCE_CHANGE | WorldUpdateDescription.FLAG_TERRAIN_SHAPE_CHANGE)) != 0 ) {
			WorldGeneratorUtils.updateSurfaceImages(
					world, 
					terrainDAO,
					bsiDAO,
					surfaceDAO,
					bsiFactory,
					area.minX, 
					area.minY, 
					area.minZ, 
					area.getWidth(), 
					area.getHeight(), 
					area.getDepth()
			);
		}
		if( (flags & WorldUpdateDescription.FLAG_ENTITY_ADDED) != 0 ) {
			// TODO update the entity positions?
		}

	}
}
